package com.ar4j.util;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.lang.time.FastDateFormat;
import org.springframework.util.ReflectionUtils;

import com.googlecode.gentyref.GenericTypeReflector;

/**
 * Various class utilities
 */
public class ClassUtils {
  private static final String SHORT_DATE_COERCING_PATTERN = "yyyy-MM-dd";
  private static final String FULL_DATE_COERCING_PATTERN = "yyyy-MM-dd HH:mm:ss";
  private static final String TIMEZONE_DATE_COERCING_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS Z";
  
  private static final DateFormat SHORT_DATE_COERCING_PARSER = new SimpleDateFormat(SHORT_DATE_COERCING_PATTERN); // parse only
  private static final DateFormat FULL_DATE_COERCING_PARSER = new SimpleDateFormat(FULL_DATE_COERCING_PATTERN); // parse only
  private static final DateFormat TIMEZONE_DATE_COERCING_PARSER = new SimpleDateFormat(TIMEZONE_DATE_COERCING_PATTERN); // parse only
  
  private static final FastDateFormat TIMEZONE_DATE_FORMATTER = FastDateFormat.getInstance(TIMEZONE_DATE_COERCING_PATTERN);
  
  private ClassUtils() {
    // UTILITY CLASS - NO CONSTRUCTOR FOR YOU!
  }
  
  /**
   * Casts the given value to the given value class, performing type conversions if required.
   * 
   * @param <E> the type of the value class
   * @param value the value to convert
   * @param requiredType the type to convert into 
   * @return the converted instance in the correct type
   */
  @SuppressWarnings("unchecked")
  public static <E> E coerceValueToType(Object value, Class<E> requiredType) {
    Object valueObject = value;
    
    // handle special type conversions
    if(value != null) {
      Class<?> sourceType = valueObject.getClass();
      if(!sourceType.equals(requiredType)) {
        if((requiredType == Byte.class || requiredType == byte.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).byteValue();
          
        } else if((requiredType == Short.class || requiredType == short.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).shortValue();
          
        } else if((requiredType == Integer.class || requiredType == int.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).intValue();
          
        } else if((requiredType == Long.class || requiredType == long.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).longValue();
          
        } else if((requiredType == Float.class || requiredType == float.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).floatValue();
          
        } else if((requiredType == Double.class || requiredType == double.class) && Number.class.isAssignableFrom(sourceType)) {
          valueObject = ((Number) valueObject).doubleValue();
          
        } else if(requiredType == Date.class && Date.class.isAssignableFrom(sourceType)) {
          valueObject = new Date(((Date) valueObject).getTime());
          
        } else if(requiredType.isEnum() && Number.class.isAssignableFrom(sourceType)) {
          int index = ((Number) valueObject).intValue();
          valueObject = requiredType.getEnumConstants()[index];
  
        } else if (sourceType == String.class && requiredType != String.class) {
          valueObject = coerceStringToType((String) valueObject, requiredType);
        
        } else if (requiredType.isAssignableFrom(sourceType)) {
          valueObject = value; // last ditch effort for assignable types sub types
          
        } else {
          throw new IllegalArgumentException("Could not coerce from " + sourceType + " to " + requiredType.getName() + " for value: " + value);
        }
      }
    }
    
    // perform final cast and return
    return (E) valueObject;
  }
  
  /**
   * Casts the given string value to the given value class, performing type conversions if required.
   * 
   * @param <E> required type
   * @param value the string to coerce
   * @param requiredType the required type
   * @return a casted or converted value
   */
  @SuppressWarnings("unchecked")
  public static <E> E coerceStringToType(String value, Class<E> requiredType) {
    Object valueObject = value;
    
    // handle special type conversions
    if(value != null) {
      if(requiredType == String.class) {
        valueObject = value;
        
      } else if(requiredType == Boolean.class || requiredType == boolean.class) {
        valueObject = Boolean.parseBoolean(value);
        
      } else if(requiredType == Byte.class || requiredType == byte.class) {
        valueObject = Byte.parseByte(value);
        
      } else if(requiredType == Short.class || requiredType == short.class) {
        valueObject = Short.parseShort(value);
        
      } else if(requiredType == Integer.class || requiredType == int.class) {
        valueObject = Integer.parseInt(value);
        
      } else if(requiredType == Long.class || requiredType == long.class) {
        valueObject = Long.parseLong(value);
        
      } else if(requiredType == Float.class || requiredType == float.class) {
        valueObject = Float.parseFloat(value);
        
      } else if(requiredType == Double.class || requiredType == double.class) {
        valueObject = Double.parseDouble(value);
        
      } else if(requiredType == Date.class) {
        valueObject = coerceStringToDate(value);
        
      } else if(requiredType.isEnum()) {
        String enumValueName = (String) valueObject;
        valueObject = Enum.valueOf((Class<? extends Enum>) requiredType, enumValueName);
        
      } else {
        throw new IllegalArgumentException("Could not coerce from String to " + requiredType.getName() + " for value: " + value);
      }
    }
    
    // perform final cast and return
    return (E) valueObject;
  }
   
  /**
   * Casts the given string value to a date, format detection is done by the string's length
   * 
   * @param value the string to coerce
   * @return a date parsed from the given string
   */
  public static Date coerceStringToDate(String value) {
    Date out = null;
    if(value != null) {
      try {
        if(value.length() == SHORT_DATE_COERCING_PATTERN.length()) {
          out = (Date) SHORT_DATE_COERCING_PARSER.parseObject(value);
        } else if(value.length() == FULL_DATE_COERCING_PATTERN.length()) {
          out = (Date) FULL_DATE_COERCING_PARSER.parseObject(value);
        } else if(value.length() >= TIMEZONE_DATE_COERCING_PATTERN.length()) {
          out = (Date) TIMEZONE_DATE_COERCING_PARSER.parseObject(value);
        } else {
          throw new IllegalArgumentException("Could not coerce from String to Date for value: " + value);
        }
      } catch(ParseException e) {
        throw new RuntimeException("Could not coerce from String to Date for value: " + value, e);
      }
    }
    
    return out;
  }
  
  /**
   * Custom conversion of a given value into a String. If the given value
   * is not a handled type (Number, Enum, Boolean, Date) its toString method
   * is called to perform the conversion.
   */
  public static String coerceTypeToString(Object value) {
    String out = null;
    if(value != null) {
      if(Boolean.class.isAssignableFrom(value.getClass())) {
        out = ((Boolean) value).toString();
        
      } else if(Number.class.isAssignableFrom(value.getClass())) {
        out = ((Number) value).toString();
        
      } else if (Enum.class.isAssignableFrom(value.getClass())) {
        out = ((Enum<?>) value).name();
        
      } else if (Date.class.isAssignableFrom(value.getClass())) {
        out = TIMEZONE_DATE_FORMATTER.format((Date) value);
        
      } else {
        out = value.toString();
        
      }
    }
    
    return out;
  }
  
  /**
   * Finds a generic superclass of the given root class, which a raw type equal to the given
   * parameterizedClass and returns its actual type arguments, or null if none are found.
   */
  public static <E> Type[] getActualTypeArguments(Class<?> root, Class<?> parameterizedClass) {
    Type base = GenericTypeReflector.getExactSuperType(root, parameterizedClass);
    if(base != null && ParameterizedType.class.isAssignableFrom(base.getClass())) {
      return ((ParameterizedType) base).getActualTypeArguments();
    }
    
    return null;
  }
  
  /**
   * Finds the given annotation on either the declared field for the given property or its
   * read method. If all else fails it tries to get the annotation from the write method, if it exists.
   * 
   * @param annotationClass annotation to retrieve
   * @param clazz class to scan
   * @param property property for which the annotation is being scanned
   * @return the annotation or null if one is not found on the given property's field or read/write methods.
   */
  public static <E extends Annotation> E getPropertyAnnotation(Class<E> annotationClass, Class<?> clazz, PropertyDescriptor property) {
    E annotation = null;
    
    // try to get from the field
    Field field = ReflectionUtils.findField(clazz, property.getName());
    if(field != null) {
      annotation = field.getAnnotation(annotationClass);
    }
        
    // otherwise try to get it from the read method
    Method readMethod = property.getReadMethod();
    if(annotation == null && readMethod != null) {
      annotation = readMethod.getAnnotation(annotationClass);
    }
    
    // still not found...try the write method as a last ditch effort
    Method writeMethod = property.getWriteMethod();
    if(annotation == null && writeMethod != null) {
      annotation = writeMethod.getAnnotation(annotationClass);
    }
    
    return annotation;
  }
  
}